using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Tomahawk.Runtime.Rendering.Objects;
using Tomahawk.Runtime.Rendering.Interfaces;
using Tomahawk.Resources.LightningBolts;
using Tomahawk.Runtime.Rendering.Materials;
using Microsoft.Xna.Framework.Content;
using Tomahawk.Runtime.Resources;

namespace Tomahawk.Runtime.Rendering.LightningBolts
{
    public sealed class LightningBolt : Entity, IRenderable
    {

        #region Members
        private bool enabled = false;
        private string descriptorName;
        private Vector3 cameraForward = Vector3.Forward;
        private Vector3 source = Vector3.Zero;
        private Vector3 destination = Vector3.Zero;
        private LightningVertex[] lightningPoints;
        private int[] indices;
        private VirtualLine[] virtualLines;
        private VirtualPoint[] virtualPoints;

        private int totalPointIndex;
        private int realVertexCount;
        private int totalRealVertices;
        private int totalIndices = 0;

        private RenderManager.Layers layer = RenderManager.Layers.Transparency;

        private SASLightningBolt sasLightningBolt;
        private DynamicVertexBuffer vertexBuffer;
        private DynamicIndexBuffer indexBuffer;
        private VertexDeclaration vertexDeclaration;

        private Random rand = new Random();
        private List<LightningSubdivisionOp> topology;
        float secondsSinceLastAnimation=0;

        RenderCommand.ExecuteCommandDelegate ExecuteRenderCommandDelegate = null;
        public event RenderMaterialDelegate OnPrepareMaterial;
        #endregion

        #region Fields

        public bool Enabled
        {
            get { return enabled; }
            set { enabled = value; }
        }

        /// <summary>
        /// Normal direction to this lightning
        /// </summary>
        public Vector3 CameraForward
        {
            get { return cameraForward; }
            set { cameraForward = Vector3.Normalize(value); }
        }

        /// <summary>
        /// Source of this lightning
        /// </summary>
        public Vector3 Source
        {
            get { return source; }
            set { source = value; }
        }

        /// <summary>
        /// Destination of this lightning
        /// </summary>
        public Vector3 Destination
        {
            get { return destination; }
            set { destination = value; }
        }

        /// <summary>
        /// Descriptor for the lightning's behaviour
        /// </summary>
        public LightningDescriptor LightningDescriptor
        {
            get { return sasLightningBolt.Behaviour; }
        }

        private float ForkArmLength
        {
            get { return sasLightningBolt.Behaviour.ForkLengthPercentage * Vector3.Distance(source, destination); }
        }

        public RenderManager.Layers Layer 
        { 
            get { return layer; } 
            set { layer = value; } 
        }

        public DynamicVertexBuffer VertexBuffer 
        { 
            get { return vertexBuffer; } 
        }

        public DynamicIndexBuffer IndexBuffer
        {
            get { return indexBuffer; }
        }

        public VertexDeclaration VertexDeclaration 
        { 
            get { return vertexDeclaration; } 
        }

        #endregion

        #region Types

        /// <summary>
        /// Private structure that keeps track of lightning segments
        /// </summary>
        private struct VirtualLine
        {
            public int v0;
            public int v1;
            public int v2;
            public int v3;
            public int widthLevel;

            public static VirtualLine Zero = new VirtualLine() { v0 = 0, v1 = 0, v2 = 0, v3 = 0, widthLevel = 0 };
        }

        /// <summary>
        /// Private structure that keeps track of lightning vertices
        /// </summary>
        private struct VirtualPoint
        {
            public int v0;
            public int v1;
            public int v2;
            public int v3;
            public int widthLevel;

            public static VirtualPoint Zero = new VirtualPoint() { v0 = 0, v1 = 0, v2 = 0, v3 = 0, widthLevel = 0 };
        }

        
        #endregion

        #region Constructors

        public LightningBolt(string descriptorName, ResourceManager.ResourcePool pool)
            : this(Vector3.Zero, Vector3.One, Vector3.Forward, descriptorName, pool)
        {
        }

        public LightningBolt(Vector3 source, Vector3 destination, Vector3 cameraForward, string descriptorName, ResourceManager.ResourcePool pool)
        {
            this.resourcePool = pool;
            this.source = source;
            this.destination = destination;
            this.cameraForward = Vector3.Normalize(cameraForward);
            this.descriptorName = descriptorName;
            this.ExecuteRenderCommandDelegate = new RenderCommand.ExecuteCommandDelegate(OnExecuteRenderCommand);
        } 

        #endregion

        #region Helper Functions
        private int ComputeNrLines(int level)
        {
            if (level == topology.Count - 1)
            {
                if (topology[level] == LightningSubdivisionOp.Jitter)
                    return 2;
                else
                    return 3;
            }

            if (topology[level] == LightningSubdivisionOp.Jitter)
                return 2 * ComputeNrLines(level + 1);
            else
                return 3 * ComputeNrLines(level + 1);
        }

        private int ComputeNrPoints(int level)
        {
            if (level == topology.Count - 1)
            {
                if (topology[level] == LightningSubdivisionOp.Jitter)
                    return 3;
                else
                    return 4;
            }

            if (topology[level] == LightningSubdivisionOp.Jitter)
                return 2 * ComputeNrPoints(level + 1) - 1;
            else
                return 3 * ComputeNrPoints(level + 1) - 2;
        }

        private float Decay(float amount, int level, float decayrate)
        {
            return amount * (float)Math.Pow(decayrate, level);
        }

        private float Random(Range range)
        {
            float nr = (float)rand.NextDouble();
            return MathHelper.Lerp(range.Min, range.Max, nr);
        }

        private Vector3 GetLeft(Vector3 forward)
        {
            Vector3 left = Vector3.Cross(cameraForward, forward);
            return left;
        }

        private Vector3 GetJittered(Vector3 start, Vector3 end, Vector3 forward, Vector3 left, int level)
        {
            Vector2 random = Vector2.Zero;
            random.X = Random(sasLightningBolt.Behaviour.JitterForwardDeviation);
            random.Y = Random(sasLightningBolt.Behaviour.JitterLeftDeviation);
            Vector2 delta = Decay(sasLightningBolt.Behaviour.JitterDeviationRadius, level, sasLightningBolt.Behaviour.JitterDecayRate) * random;

            return Vector3.Lerp(start, end, Random(sasLightningBolt.Behaviour.SubdivisionFraction)) + delta.X * forward + delta.Y * left;
        }
        private Vector3 GetForkDelta(Vector3 forward, Vector3 left, int level)
        {
            Vector2 random = Vector2.Zero;
            random.X = Random(sasLightningBolt.Behaviour.ForkForwardDeviation);
            random.Y = Random(sasLightningBolt.Behaviour.ForkLeftDeviation);
            Vector2 fork_delta = Decay(ForkArmLength, level, sasLightningBolt.Behaviour.ForkDecayRate) * random;

            return fork_delta.X * forward + fork_delta.Y * left;
        }

        private int BuildVertices(int level, Vector3 start, Vector3 end, int virtualLineIndex)
        {
            if (level == topology.Count)
            {
                SetLinePositions(virtualLineIndex, start, end);
                return virtualLineIndex + 1;
            }

            int lastLineIndex = virtualLineIndex;

            switch (topology[level])
            {
                case LightningSubdivisionOp.Jitter:
                    lastLineIndex = JitterStep(level, start, end, virtualLineIndex);
                    break;
                case LightningSubdivisionOp.JitterAndFork:
                    lastLineIndex = ForkStep(level, start, end, virtualLineIndex);
                    break;
            }
            return lastLineIndex;

        }

        private int JitterStep(int level, Vector3 start, Vector3 end, int virtualLineIndex)
        {
            Vector3 forward = Vector3.Normalize(end - start);
            Vector3 left = GetLeft(forward);

            Vector3 jittered = GetJittered(start, end, forward, left, level);

            int lastLineIndex;
            SetPointPositions(jittered, forward, left);
            lastLineIndex = BuildVertices(level + 1, start, jittered, virtualLineIndex);
            lastLineIndex = BuildVertices(level + 1, jittered, end, lastLineIndex);
            return lastLineIndex;
        }


        private int ForkStep(int level, Vector3 start, Vector3 end, int virtualLineIndex)
        {
            Vector3 forward = Vector3.Normalize(end - start);
            Vector3 left = GetLeft(forward);

            Vector3 jittered = GetJittered(start, end, forward, left, level);
            Vector3 forked = jittered + GetForkDelta(forward, left, level);

            int lastLineIndex;
            SetPointPositions(jittered, forward, left);
            SetPointPositions(forked, forward, left);
            lastLineIndex = BuildVertices(level + 1, start, jittered, virtualLineIndex);
            lastLineIndex = BuildVertices(level + 1, jittered, forked, lastLineIndex);
            lastLineIndex = BuildVertices(level + 1, jittered, end, lastLineIndex);
            return lastLineIndex;
        }

        private float ComputeWidth(int widthLevel)
        {
            if (sasLightningBolt.Behaviour.IsWidthDecreasing)
                return sasLightningBolt.Behaviour.BaseWidth / widthLevel;
            else
                return sasLightningBolt.Behaviour.BaseWidth;
        }

        private void SetLinePositions(int virtualLineIndex, Vector3 start, Vector3 end)
        {
            int v0 = virtualLines[virtualLineIndex].v0;
            int v1 = virtualLines[virtualLineIndex].v1;
            int v2 = virtualLines[virtualLineIndex].v2;
            int v3 = virtualLines[virtualLineIndex].v3;

            Vector3 forward = Vector3.Normalize(end - start);
            Vector3 left = GetLeft(forward);
            float width = ComputeWidth(virtualLines[virtualLineIndex].widthLevel);

            lightningPoints[v0].Position = start + left * width;
            lightningPoints[v1].Position = end + left * width;
            lightningPoints[v2].Position = end - left * width;
            lightningPoints[v3].Position = start - left * width;
        }
        private void SetPointPositions(Vector3 position, Vector3 forward, Vector3 left)
        {
            int pointIndex = totalPointIndex;
            int v0 = virtualPoints[pointIndex].v0;
            int v1 = virtualPoints[pointIndex].v1;
            int v2 = virtualPoints[pointIndex].v2;
            int v3 = virtualPoints[pointIndex].v3;

            float width = ComputeWidth(virtualPoints[pointIndex].widthLevel);

            lightningPoints[v0].Position = position + width * (-forward - left);
            lightningPoints[v1].Position = position + width * (forward - left);
            lightningPoints[v2].Position = position + width * (forward + left);
            lightningPoints[v3].Position = position + width * (-forward + left);
            totalPointIndex++;
        }


        private void AddPoint(int width)
        {
            int pointIndex = totalPointIndex;
            int v0, v1, v2, v3;
            v0 = totalRealVertices;
            v1 = totalRealVertices + 1;
            v2 = totalRealVertices + 2;
            v3 = totalRealVertices + 3;

            virtualPoints[pointIndex] = VirtualPoint.Zero;

            virtualPoints[pointIndex].v0 = v0;
            virtualPoints[pointIndex].v1 = v1;
            virtualPoints[pointIndex].v2 = v2;
            virtualPoints[pointIndex].v3 = v3;
            virtualPoints[pointIndex].widthLevel = width;

            lightningPoints[v0].TextureCoordinates.X = 0;
            lightningPoints[v0].TextureCoordinates.Y = 0;
            lightningPoints[v0].ColorGradient.X = -1;
            lightningPoints[v0].ColorGradient.Y = 1;

            lightningPoints[v1].TextureCoordinates.X = 1;
            lightningPoints[v1].TextureCoordinates.Y = 0;
            lightningPoints[v1].ColorGradient.X = 1;
            lightningPoints[v1].ColorGradient.Y = 1;

            lightningPoints[v2].TextureCoordinates.X = 1;
            lightningPoints[v2].TextureCoordinates.Y = 1;
            lightningPoints[v2].ColorGradient.X = 1;
            lightningPoints[v2].ColorGradient.Y = -1;

            lightningPoints[v3].TextureCoordinates.X = 0;
            lightningPoints[v3].TextureCoordinates.Y = 1;
            lightningPoints[v3].ColorGradient.X = -1;
            lightningPoints[v3].ColorGradient.Y = -1;

            indices[totalIndices] = v0;
            indices[totalIndices + 1] = v1;
            indices[totalIndices + 2] = v2;

            indices[totalIndices + 3] = v0;
            indices[totalIndices + 4] = v2;
            indices[totalIndices + 5] = v3;

            totalRealVertices += 4;
            totalIndices += 6;
            totalPointIndex++;
        }

        private void AddLine(int virtualLineIndex, int width)
        {
            int v0, v1, v2, v3;
            v0 = totalRealVertices;
            v1 = totalRealVertices + 1;
            v2 = totalRealVertices + 2;
            v3 = totalRealVertices + 3;

            virtualLines[virtualLineIndex] = VirtualLine.Zero;

            virtualLines[virtualLineIndex].v0 = v0;
            virtualLines[virtualLineIndex].v1 = v1;
            virtualLines[virtualLineIndex].v2 = v2;
            virtualLines[virtualLineIndex].v3 = v3;
            virtualLines[virtualLineIndex].widthLevel = width;

            lightningPoints[v0].TextureCoordinates.X = 0;
            lightningPoints[v0].TextureCoordinates.Y = 0;
            lightningPoints[v0].ColorGradient.X = 1;
            lightningPoints[v0].ColorGradient.Y = 0;

            lightningPoints[v1].TextureCoordinates.X = 1;
            lightningPoints[v1].TextureCoordinates.Y = 0;
            lightningPoints[v1].ColorGradient.X = 1;
            lightningPoints[v1].ColorGradient.Y = 0;

            lightningPoints[v2].TextureCoordinates.X = 1;
            lightningPoints[v2].TextureCoordinates.Y = 1;
            lightningPoints[v2].ColorGradient.X = -1;
            lightningPoints[v2].ColorGradient.Y = 0;

            lightningPoints[v3].TextureCoordinates.X = 0;
            lightningPoints[v3].TextureCoordinates.Y = 1;
            lightningPoints[v3].ColorGradient.X = -1;
            lightningPoints[v3].ColorGradient.Y = 0;

            indices[totalIndices] = v0;
            indices[totalIndices + 1] = v1;
            indices[totalIndices + 2] = v2;

            indices[totalIndices + 3] = v0;
            indices[totalIndices + 4] = v2;
            indices[totalIndices + 5] = v3;

            totalRealVertices += 4;
            totalIndices += 6;
        }

        private int BuildIndices(int level, int lineIndex, int width)
        {
            if (level == topology.Count)
            {
                AddLine(lineIndex, width);
                return lineIndex + 1;
            }
            int lastLineIndex = 0;

            switch (topology[level])
            {
                case LightningSubdivisionOp.Jitter:
                    AddPoint(width);
                    lastLineIndex = BuildIndices(level + 1, lineIndex, width);
                    lastLineIndex = BuildIndices(level + 1, lastLineIndex, width);
                    break;
                case LightningSubdivisionOp.JitterAndFork:
                    AddPoint(width);
                    AddPoint(width + 1);
                    lastLineIndex = BuildIndices(level + 1, lineIndex, width);
                    lastLineIndex = BuildIndices(level + 1, lastLineIndex, width + 1);
                    lastLineIndex = BuildIndices(level + 1, lastLineIndex, width);
                    break;
                default:
                    break;
            }
            return lastLineIndex;
        }


        
        #endregion

        /// <summary>
        /// Update Lightning Animation
        /// </summary>
        public void Update(float elapsedTime)
        {
            if (!enabled)
            {
                return;
            }

            if (sasLightningBolt.Behaviour.AnimationFramerate == 0.0f)
            {
                return;
            }

            if (sasLightningBolt.Behaviour.AnimationFramerate == -1.0f)
            {
                totalPointIndex = 0;

                Vector3 forward = Vector3.Normalize(destination - source);
                Vector3 left = GetLeft(forward);

                SetPointPositions(source, forward, left);
                SetPointPositions(destination, forward, left);
                BuildVertices(0, source, destination, 0);

                vertexBuffer.SetData(0, lightningPoints, 0, lightningPoints.Length, LightningVertex.SizeInBytes, SetDataOptions.NoOverwrite);
                indexBuffer.SetData(0, indices, 0, indices.Length, SetDataOptions.NoOverwrite);
            }
            else
            {
                secondsSinceLastAnimation += elapsedTime;
                float frameLength = 1.0f / sasLightningBolt.Behaviour.AnimationFramerate;
                if (secondsSinceLastAnimation > frameLength)
                {
                    secondsSinceLastAnimation -= frameLength;
                    totalPointIndex = 0;

                    Vector3 forward = Vector3.Normalize(destination - source);
                    Vector3 left = GetLeft(forward);

                    SetPointPositions(source, forward, left);
                    SetPointPositions(destination, forward, left);
                    BuildVertices(0, source, destination, 0);

                    vertexBuffer.SetData(0, lightningPoints, 0, lightningPoints.Length, LightningVertex.SizeInBytes, SetDataOptions.NoOverwrite);
                    indexBuffer.SetData(0, indices, 0, indices.Length, SetDataOptions.NoOverwrite);
                }
            }
        }

        #region IRenderable Members

        public void OnExecuteRenderCommand(SASMaterial material)
        {
            Vector4 interiorColor = sasLightningBolt.Behaviour.InteriorColor.ToVector4();
            Vector4 exteriorColor = sasLightningBolt.Behaviour.ExteriorColor.ToVector4();
            material.Effect.Parameters["StartColor"].SetVectorValue(ref interiorColor);
            material.Effect.Parameters["EndColor"].SetVectorValue(ref exteriorColor);
            if (OnPrepareMaterial != null) OnPrepareMaterial(this, material);
        }

        public void Render(ref Matrix worldMatrix, GraphicsDevice myDevice)
        {
            if (!enabled)
            {
                return;
            }

            if (vertexBuffer.IsContentLost)
            {
                vertexBuffer.SetData(lightningPoints);
            }

            if (indexBuffer.IsContentLost)
            {
                indexBuffer.SetData(indices);
            }

            RenderManager manager = Engine.Instance.RenderManager;

            RenderCommand command = Engine.Instance.RenderManager.BuildRenderCommand(
                this,
                lightningPoints.Length,
                indices.Length / 3,
                ref worldMatrix,
                sasLightningBolt);

            command.OnExecuteCommand += ExecuteRenderCommandDelegate;
            manager.AddCommand(command, layer);
        }

        #endregion

        protected override bool OnLoad(GraphicsDevice graphics, ContentManager contentManager)
        {
            int lineCount;
            int pointCount;

            LightningDescriptor descriptor = contentManager.Load<LightningDescriptor>(descriptorName);

            if (descriptor == null)
            {
                return false;
            }

            SASEffect sasEffect = new SASEffect();
            sasEffect.Initialize(SASLightningBolt.EffectFile, contentManager);
            sasLightningBolt = new SASLightningBolt(sasEffect, descriptor);

            topology = sasLightningBolt.Behaviour.Topology;

            lineCount = ComputeNrLines(0);
            pointCount = ComputeNrPoints(0);
            virtualLines = new VirtualLine[lineCount];
            virtualPoints = new VirtualPoint[pointCount];

            indices = new int[lineCount * 6 + pointCount * 6];
            realVertexCount = lineCount * 4 + pointCount * 4;

            lightningPoints = new LightningVertex[realVertexCount];

            lock (Engine.ResourceLock)
            {
                vertexDeclaration = new VertexDeclaration(graphics, LightningVertex.VertexElements);
                vertexBuffer = new DynamicVertexBuffer(graphics, realVertexCount * LightningVertex.SizeInBytes, BufferUsage.WriteOnly);
                indexBuffer = new DynamicIndexBuffer(graphics, indices.Length * sizeof(int), BufferUsage.WriteOnly, IndexElementSize.ThirtyTwoBits);
            }

            totalRealVertices = 0;
            totalPointIndex = 0;

            AddPoint(1);
            AddPoint(1);
            BuildIndices(0, 0, 1);

            totalPointIndex = 0;

            Vector3 forward = Vector3.Normalize(destination - source);
            Vector3 left = GetLeft(forward);

            SetPointPositions(source, forward, left);
            SetPointPositions(destination, forward, left);
            BuildVertices(0, source, destination, 0);

            return true;
        }
    }
}
